Skip to content

fix(claude-code-review): hand the reviewer the PR diff as a file - #567

Merged
d-morrison merged 3 commits into
mainfrom
claude/gia-on-gha-ta3ku7
Aug 21, 2026
Merged

fix(claude-code-review): hand the reviewer the PR diff as a file#567
d-morrison merged 3 commits into
mainfrom
claude/gia-on-gha-ta3ku7

Conversation

@d-morrison

@d-morrison d-morrison commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Closes #543.

The diagnosis this changes

The reviewer is not attempting something forbidden. It wants the diff in a file so it can chunk or count a large one, and it has no route to one:

  • Bash(gh pr diff:*) is allowed, but a command chaining it with >, |, or ; is rejected as a whole, even though the allowed part matches exactly.
  • The agent cannot write a file itself — /tmp and a mkdir-created directory are both hard blocks in that sandbox, not permission prompts.
  • The harness does save the output of a successful bare call past ~30,000 characters, but that only exists after the call the reviewer is trying to shortcut.

So it retries variants of the same redirect until the denial count crosses the stub-retry threshold, the run ends with no verdict, and require-review goes red.

Measured three times:

Where Denials Cost Verdict
ucdavis/win#78, run 32415477507 33, from 15+ variants $2.29 none
#543's own reproduction 6 $3.42 none
#555, run 32526051617 8 $4.95 none

The third is why this is a step rather than another rewording. #542's prompt paragraph forbids exactly that construction, in detail, and was already live at @v2 when it happened. That is CLAUDE.md's "a prompt instruction is a request; a permission rule is a constraint" landing on the other foot: here the constraint exists and the capability the reviewer wants is missing.

The change

A step writes the diff to the workspace before the agent starts, and the prompt names that path — telling the reviewer to read it, to ignore it when listing changed files, and to fall back to gh pr diff only for something the file does not contain (a different base, a single path).

That removes the motivation rather than the capability. Bash(gh pr diff:*) stays allowed, so a denial afterwards means something has genuinely gone wrong — which is what the starvation threshold is trying to measure.

Not fatal on failure, deliberately. The reviewer keeps the bare command it has today, so the worst case is the behaviour that exists now rather than a red check. Verified against a stub gh across four shapes:

gh behaviour path output file on disk exit
succeeds with a diff set present 0
succeeds, writes nothing empty absent 0
fails empty absent 0
writes bytes, then fails empty removed 0

The last row is the one that matters: a truncated diff read as a complete one is worse than no file at all.

Written into the workspace rather than $RUNNER_TEMP because the agent reads relative to its working directory, and dot-prefixed so it reads as tooling rather than repository content.

Retracting what I proposed on the issue

I twice proposed dropping Bash(gh pr diff:*) from the allowlist "so the pre-written diff file is the only path". There is no pre-written diff file — I read "a file already saved for you" in the action's own comment as a standing artifact when it is a consequence of the successful call. That change would have deleted the reviewer's only route to the diff. Retracted on the issue.

#543's open question is now answerable, and the answer is not Task

The body says it could not confirm its own hypothesis because the log recorded only a count — which is #540, since shipped. The first named instance is #555's:

Bashx8 (sample: Bash: gh pr diff 555 --repo Morrison-Lab/gha > /tmp/pr555.diff 2>&1; wc -l /tmp/pr555.diff)

All Bash, no Task. Consistent with that, the same PR's round-1 review reported working via four parallel agents and produced two well-founded findings, so those calls were being permitted. This does not retroactively explain wai#83's 12 denials, whose run predates the naming.

Cannot be verified on this PR

claude-review.yml pins claude-code-review.yml@v2, so this PR's own review runs the pre-fix workflow — the documented self-verify gap. The review should still run (the self_mod guard keys on claude-review.yml, which this diff does not touch), it just cannot exercise the change. Verification is a consumer PR after the next tag slide.

Passing pr-diff-file to run-claude-review-attempt@v2 before the tag moves is safe: an undeclared input on a composite action is a warning, unlike a reusable workflow's with:, which fails the call gate.

Deferred

The save step is inline shell rather than a tested script. Extracting it would mean a new composite action, since a reusable workflow cannot reference this repo's scripts/ by path (the consumer's checkout is what actions/checkout produces — gha#191/#196), and a new composite cannot gain its first @v2 caller in the PR that introduces it (gha#326/#327). Filed as #568 rather than forced in here; the four-shape stub run above is the coverage in the meantime.

claude added 2 commits August 21, 2026 22:18
Closes #543.

The reviewer is not attempting something forbidden. It wants the diff in
a FILE so it can chunk or count a large one, and it has no route to one:
`Bash(gh pr diff:*)` is allowed, but chaining it with a redirect or pipe
is rejected as a whole command, and the agent cannot write a file itself
-- /tmp and a mkdir-created directory are both hard blocks in that
sandbox rather than permission prompts. So it retries variants of the
same redirect until the denial count crosses the stub-retry threshold
and the run ends with no verdict.

Measured three times:

  ucdavis/win#78  run 32415477507   15+ variants, 33 denials, $2.29
  gha#543 repro                      6 denials, $3.42
  gha#555         run 32526051617    8 denials, $4.95

The third is why this is a step rather than another rewording: the
prompt paragraph forbidding exactly that construction (#542) was already
live at @v2 when it happened, which is CLAUDE.md's "a prompt instruction
is a request; a permission rule is a constraint" landing on the other
foot -- here the constraint exists and the capability the reviewer wants
is missing.

So supply it. A new step writes the diff to the workspace before the
agent starts, and the prompt names that path and tells the reviewer to
read it, to ignore it when listing changed files, and to fall back to
`gh pr diff` only for something the file does not contain.

Not fatal on failure, deliberately: the reviewer keeps the bare command
it has today, so the worst case is current behaviour rather than a red
check. Verified against a stub `gh` across four shapes --

  gh succeeds          -> path set, file present
  gh writes nothing    -> path empty, no file
  gh fails             -> path empty, no file, exit 0
  gh writes then fails -> path empty, PARTIAL FILE REMOVED, exit 0

-- the last being the one that matters, since a truncated diff read as a
complete one is worse than no file at all.

Written into the workspace rather than $RUNNER_TEMP because the agent
reads relative to its working directory, and dot-prefixed so it reads as
tooling rather than repository content.

Retracts the fix I proposed on #543 twice, which was to drop
`Bash(gh pr diff:*)` from the allowlist "so the pre-written diff file is
the only path". There was no such file: the harness saves one only after
a SUCCESSFUL bare call exceeds the inline ceiling, so removing the grant
would have deleted the reviewer's only route to the diff.

Also worth recording on the issue rather than only here: #540's tool-name
logging has made #543's own open question answerable, and the answer is
not the `Task` fan-out it hypothesised. gha#555's denials were named
`Bashx8`, all redirect-chained, with no `Task` denials at all -- and that
run's own review reported working via four parallel agents.
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

Review round 1 on #567.

FINDING -- missing changelog fragment. Correct and quotable: CLAUDE.md
requires one for user-facing behaviour, and #541's own
reviewer-diff-redirect-stub.fixed.md is direct precedent for a change of
exactly this shape. Added changelog.d/reviewer-pr-diff-file.fixed.md.

Also acted on one of the two observations the review explicitly did NOT
flag as blocking, because it decides whether the fix works at all: the
step emitted a workspace-relative path while Claude Code's Read tool
requires an absolute `file_path`. A reviewer handed a relative path
would have to resolve it before reading the file this change exists to
put in front of it -- and a failed Read is exactly the kind of dead end
that sends it back to redirect variants. Now emits "$GITHUB_WORKSPACE/
$DIFF_NAME", with the prompt and the input description saying absolute
and why.

Left the other observation alone, deliberately. The step uses
`set -uo pipefail` rather than the file's `set +e -uo pipefail` idiom for
always-exit-0 steps; the review confirmed no command in either branch can
fail, and the `if` already handles the only failure that matters. Adding
`+e` would suggest a hazard that is not there.

Copy link
Copy Markdown
Collaborator Author

Round 1 addressed in ee5cbe6

The finding — missing changelog fragment. Correct, and quotable: CLAUDE.md requires one for user-facing behaviour, and #541's own changelog.d/reviewer-diff-redirect-stub.fixed.md is direct precedent for a change of exactly this shape. Added changelog.d/reviewer-pr-diff-file.fixed.md.

One of the two observations you explicitly did not flag, acted on anyway — the relative-path one, because it decides whether the fix works at all rather than costing style points.

You noted the prompt says the saved path is "relative to your working directory" while Read wants an absolute path, and called it a possible one-step cost. It is worse than that in the failure case this PR exists to prevent: a reviewer handed a path Read rejects has to work out why, and the behaviour that dead end historically produces is going back to redirect variants. The whole point of the change is that the file is trivially reachable.

So the step now emits $GITHUB_WORKSPACE/$DIFF_NAME, and both the prompt and the input description say absolute and why.

The other observation I left alone, deliberately. The step uses set -uo pipefail rather than this file's set +e -uo pipefail idiom for always-exit-0 steps. Your own check is right that no command in either branch can fail, and the if already handles the only failure that matters. Adding +e would imply a hazard that is not there, so the idiom mismatch is the honest state.

On the invalidated security finding — worth saying that raising and then killing it was the useful outcome, not wasted effort. .claude-review-pr.diff being untracked in the workspace is exactly the shape that would be dangerous if any step in this job swept broadly, and confirming that track_progress is hardcoded false here and that the only git add -A-style sweep lives in the unrelated claude.yml is the check I would want run before merging this.

Two things this PR still cannot do, restated so they are not mistaken for oversights: it cannot exercise its own fix (claude-review.yml pins @v2, so this review ran the pre-fix workflow), and the save step is inline shell rather than a tested script, which needs a new composite action and therefore a follow-up after the tag slides — #568.


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

Claude finished reviewView run


Review: Morrison-Lab/gha PR #567

Status checked live: open, unmerged (head ee5cbe6)

Summary

This is a confirming round. The prior review (posted 2026-08-21T22:33:34Z, #567 (comment)) reviewed head 828cb1f and returned a single finding — a missing changelog.d/ fragment — with everything else (design, gating, error handling, security) checked out clean across four independent passes.

Commit ee5cbe6 addresses that round, and does slightly more than the finding asked for:

  1. The finding is fixed. changelog.d/reviewer-pr-diff-file.fixed.md was added, following the <slug>.<category>.md convention documented in changelog.d/README.md (category fixed is in the allowed set), with direct precedent (reviewer-diff-redirect-stub.fixed.md for claude-code-review: reviewer loops ~15x failing to save a large PR diff to disk, exhausting the denial budget #541) cited in the author's own reply. Content checked against the actual diff — the $4.95/8-denial gha#555 figure it cites matches the number already baked into the new step's own code comment in .github/workflows/claude-code-review.yml.

  2. One of the two previously-unflagged observations was also fixed, unprompted. The prior review noted (as a non-blocking observation, not a finding) that the prompt told the reviewer the saved diff path was "relative to your working directory" while Claude Code's Read tool needs an absolute path. This round's diff shows that's now resolved consistently across all three sites:

    • run-claude-review-attempt/action.yml's pr-diff-file input description now reads "Absolute path of a file... Absolute because Claude Code's Read tool requires an absolute file_path."
    • The prompt block says "an absolute path you can pass straight to Read."
    • claude-code-review.yml's "Save the PR diff for the reviewer" step now builds target="$GITHUB_WORKSPACE/$DIFF_NAME" (which is absolute, since GITHUB_WORKSPACE always is) rather than a bare relative filename, with a comment explaining why.

    I re-grepped the diff for every "relative"/"absolute" mention to confirm no stale wording survived; the one remaining "relative to its working directory" phrase (in the step's leading comment block) is explaining the unrelated choice of $GITHUB_WORKSPACE over $RUNNER_TEMP as the write location, not describing the path format handed to Read — it doesn't contradict the fix.

  3. The other observation (the set -uo pipefail vs. this file's usual set +e -uo pipefail idiom) was deliberately left alone, and the author's reply gives the correct reasoning: no command in either branch of that step can actually fail, so adding +e would assert a hazard that isn't there. Consistent with the prior review's own read that this wasn't a live bug.

No other lines changed. I confirm via gh pr diff 567 --name-only that exactly three files are touched: the two workflow/action files (functionally identical to the prior round except for the absolute-path fix above) plus the new changelog fragment — nothing else regressed or was reopened.

Verdict

Ready for merge — the prior round's sole finding is fixed, one of its non-blocking observations was proactively addressed as well, and no new issues were introduced in this round.

Reviewed commit: ee5cbe6

@github-actions

Copy link
Copy Markdown
Contributor

💰 Cost: $1.3463 (review) — run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

claude-review: gha#198's high-denial stub signature recurred (12 denials, docs-only diff, nothing posted)

2 participants